Cloudflare Caching Demo — Cheat Sheet
~15–20 min · Show caching going from DYNAMIC → HIT live · End at the dashboard analytics
Pre-demo setup checklist
- Logged into Cloudflare dashboard ·
tarheel.us zone
- Terminal open with big font · ready to run
curl -sI
- Browser tab queued to Caching → Cache Rules
- Second browser tab queued to Analytics → Performance (or Caching → Overview)
- Confirm starting state:
curl -sI https://nginx.tarheel.us/ | grep cf-cache should show DYNAMIC (HTML not cached by default)
- If a Cache Rule already exists for nginx.tarheel.us, delete it before demo so the BEFORE state is real
1. Open (~1 min)
"Caching is the part of Cloudflare that already works for almost every customer — they just don't know how much. Static assets like images, CSS, JavaScript get cached automatically at 330+ data centers globally. What most customers miss is that HTML pages, API responses, and dynamic content can be cached too — they just need a rule."
"I'm going to show you a page that's NOT being cached right now, turn on caching in two clicks, and you'll see the difference in the dashboard immediately."
2. The Before State — HTML Going to Origin (~2 min)
BEFORE — every request goes to origin
▶ Switch to terminal
curl -sI https://nginx.tarheel.us/ | grep -iE "cf-cache|cf-ray|age"
"Output should show: cf-cache-status: DYNAMIC"
"DYNAMIC means Cloudflare didn't even try to cache this. Why? Because the URL doesn't have a file extension. Cloudflare's default cache keys off extensions — .png, .css, .js, .jpg. A URL like / or /about has no extension, so Cloudflare assumes it could be personalized — login pages, dashboards, anything dynamic — and sends every request straight to origin."
"Safe but slow. Every visitor to that homepage is hitting my origin server, even though the page hasn't changed in days."
▶ Run it 3 times in a row to drive the point home:
for i in 1 2 3; do curl -sI https://nginx.tarheel.us/ | grep cf-cache; done
"Three requests. Three DYNAMICs. Three round trips to my origin. Now let's fix it."
3. Add a Cache Rule (~3 min)
CONFIGURE — turn on HTML caching with one rule
▶ Dashboard: tarheel.us → Caching → Cache Rules → Create rule
Fill in:
▶ Click Deploy
"That's the entire change. One expression, one TTL, deployed globally in seconds. Cloudflare just pushed this rule to all 330+ data centers."
"Notice what we didn't have to do — touch the origin, modify any code, change any HTTP headers. The Cache Rule lives at the edge and overrides whatever defaults were in place."
4. The After State — Watch It Cache (~3 min)
AFTER — first request MISS, every request after is HIT
▶ Back to terminal
for i in 1 2 3 4; do curl -sI https://nginx.tarheel.us/ | grep cf-cache; sleep 1; done
"Watch the output:"
cf-cache-status: MISS ← first request, Cloudflare fetched from origin and stored it
cf-cache-status: HIT ← served from the edge
cf-cache-status: HIT ← served from the edge
cf-cache-status: HIT ← served from the edge
"There it is. One MISS to populate the cache, then every request after is a HIT served from the data center closest to me. The origin is no longer involved in 75% of these requests."
"In a real scenario with thousands of visitors, the ratio is even more dramatic — maybe one MISS for every thousand HITs."
▶ For an even better visual, run a verbose curl to show the age header growing:
curl -sI https://nginx.tarheel.us/ | grep -iE "cf-cache|age:"
"The age header tells you how many seconds ago Cloudflare cached it. Hit refresh a few times and watch it climb. That's edge time, not origin time."
5. Show It in the Dashboard (~3 min)
▶ Dashboard: tarheel.us → Analytics & Logs → Traffic (or Caching → Overview)
"Now let's pull up the analytics. This is where you can see caching working at scale — across every request, not just the ones we just ran."
What to point at:
- Cached vs. Uncached Requests — the donut/pie chart showing how much traffic Cloudflare served vs. how much went to origin. Higher cached % = lower origin load + faster page loads.
- Cached Bandwidth — how much bandwidth Cloudflare absorbed for you. "Every byte here is a byte your origin didn't have to send. If you're paying for egress on AWS or GCP, this is real money saved."
- Requests over time — toggle between total and cached. You'll see the cached line ride along with total — the gap is what hit origin.
- Top Cached & Uncached Resources — shows you exactly what's being cached and what isn't, so you can target Cache Rules where they'll have impact.
"If you're a CFO looking at this page, what you should be asking is: 'how much smaller could my origin server be if Cloudflare absorbed even more of this?' That's the conversation."
"And remember — we made one change. One rule. No code deployed, no servers restarted, no DNS modified."
THE ARCHITECTURAL POINT
Static assets → cached automatically. No config needed.
HTML / dynamic content → one Cache Rule away.
Edge cache → 330+ locations. Fastest path to every visitor.
Origin load → drops by the size of your cache hit ratio.
"Most customers are already paying for Cloudflare. The bandwidth, the data centers, the cache — it's already there. The question is whether you're using it."
6. If Customer Asks — cf-cache-status Decoder
| Value | What it means |
| HIT | Served from Cloudflare cache. The good outcome. |
| MISS | Not in cache. Cloudflare fetched from origin and (usually) stored it. |
| BYPASS | Something told Cloudflare not to cache — usually a cookie, header, or rule. |
| DYNAMIC | Cloudflare didn't try to cache. Default for URLs without static file extensions. |
| EXPIRED | Was cached, but TTL ran out. Will refresh from origin. |
| REVALIDATED | Was cached, TTL expired, origin confirmed it's still fresh (304). |
| UPDATING | Stale-while-revalidate: served from cache while async refresh happens. |
| STALE | Origin is down — served the stale cache anyway. Lifesaver. |
| NONE/Unknown | Cloudflare didn't cache (usually a non-GET method or zero-TTL). |
You'll NOT see cf-cache-status on 3xx redirects, WAF blocks, Cloudflare error pages, or Worker fetch requests.
Q&A — back-pocket answers
What products fit which use case?
Static web content (images, CSS, JS, fonts, video) — Default cache, already on.
HTML pages, WordPress — APO (~$5/mo). Auto-purge on publish.
HTML pages, static site (Jekyll/Hugo/Astro) — Cache Rule, long Edge TTL, purge on deploy.
HTML pages, dynamic with logged-in users — Cache Rule that bypasses on auth cookie.
API responses, public — Cache Rules. If logic is complex, Workers + Cache API.
API responses, per-user — Workers KV. Cache the data, not the response.
Large files accessed frequently — Default cache + Tiered Cache.
Large files accessed rarely — Cache Reserve.
Object storage — R2. Zero egress fees.
App state inside a Worker — KV (eventually consistent) or Durable Objects (strongly consistent).
"My cache hit rate is low" — how do I diagnose?
- Check
cf-cache-status. MISS/BYPASS/DYNAMIC point to different causes.
- Check origin Cache-Control headers.
private and no-store prevent CDN caching.
- Check query strings — high cardinality fragments the cache.
- Check cookies — origin setting cookies can cause BYPASS.
- Check Vary headers — fragments cache by header value.
curl -svo /dev/null https://example.com/asset.png 2>&1 | \
egrep -i '< cf-cache|< cache-control|< age|< vary|< set-cookie'
"My HTML isn't being cached"
HTML is NOT cached by default — only static assets by file extension. To cache HTML, create a Cache Rule:
- Expression: hostname equals your domain (or specific paths)
- Cache Eligibility: Eligible for cache
- Edge Cache TTL: Override origin and set your own value
If logged-in users see cached pages, add a bypass on auth cookie: AND cookie does not contain session_id
"My origin is being hammered"
- Turn on Tiered Cache (free, one toggle). Routes cache misses through upper-tier DCs first — fewer origin requests.
- Enable Cache Reserve. Persistent storage for content that gets evicted. Requires TTL ≥ 10 hours + Content-Length header.
- Audit Cache-Control headers — make sure origin isn't accidentally killing caching.
"I need to purge cache after a deploy"
Ranked by precision (most surgical first):
- Purge by Cache Tag — Enterprise. Cleanest for grouped invalidation.
- Purge by URL — surgical. Best for specific assets.
- Purge by Prefix — e.g.,
/static/
- Purge by Hostname — useful for staging deployments.
- Purge Everything — nuclear option.
All available via API for CI/CD integration.
"My cache-control header is being ignored"
- Free/Pro/Business: Origin Cache Control enabled by default.
- Enterprise: Must be manually enabled via Cache Rules.
- Cache Rules with "Ignore cache-control header and use this TTL" override origin.
- If origin sends NO cache-control, Cloudflare caches by HTTP status code defaults (200 = 120 minutes).
"Why is private content being cached?" / "How do I keep private content out of cache?"
- Check Cache-Control on origin — should be
private or no-store for user-specific content.
- Check Cache Rules — might be overriding origin.
- Check Vary: Cookie header — fragments cache by cookie, prevents leakage.
- Most secure: serve personalized content via Workers + KV, not HTTP cache.
Header precedence (which directive wins?)
- Cloudflare-CDN-Cache-Control (Cloudflare only)
- CDN-Cache-Control (any CDN)
- Cache-Control: s-maxage (shared caches)
- Cache-Control: max-age (any cache)
- Expires header (legacy, lowest priority)
If origin sends no directives at all, Cloudflare caches based on HTTP status code defaults.
"I want different TTLs for browser vs. Cloudflare"
Two ways:
- Origin sends both
max-age (browser) and s-maxage (CDN). Example: max-age=0, s-maxage=86400.
- Cache Rules → set Edge Cache TTL and Browser Cache TTL independently.
"I want to cache differently per language / device / country"
Cache Rules → Custom Cache Key. Include in cache key:
- Language (Accept-Language header)
- Device type (mobile / desktop / tablet)
- Country (CF-IPCountry)
- Custom headers or cookies
Be careful with cardinality — too many variations fragment the cache.
Tiered Cache vs. Cache Reserve — which when?
Tiered Cache — free. Reduces requests to origin by having edge DCs check upper-tier DCs first. Recommend this first when origin is getting hammered.
Cache Reserve — paid (usage-based on storage + operations). Persistent storage that holds content even after it would normally be evicted. Recommend for:
- Expensive-to-regenerate content (large videos, archives, generated reports)
- Long-tail content accessed infrequently but should always be available
- High egress fees to cloud provider for origin pulls
Requires TTL ≥ 10 hours + Content-Length header. Tiered Cache strongly recommended alongside.
Workers + Cache API — when do I need it?
Use when Cache Rules aren't expressive enough. From inside a Worker you can:
- Cache responses that normally wouldn't be (POST, dynamically generated)
- Set custom TTLs and cache keys
- Modify content before caching
- Serve different cached content per user / geography / headers
Three methods: cache.match(), cache.put(), cache.delete().
When to push back on a customer ask
"Cache everything for 24 hours" — will serve stale logged-in pages and expose private info. Ask what changes when.
"Cache POST/PUT/DELETE requests" — those are mutations. Cache GET only.
"Cache by IP for personalization" — IPs are shared via CGNAT, proxies, NAT. Use cookies, headers, or Workers + KV.
"Should I use Page Rules or Cache Rules?" — Cache Rules. Page Rules are legacy.
"I want max-age=0 to mean don't cache" — max-age=0 means cached but immediately stale. Use no-store if you mean don't cache.
"I'll just bypass cache for everything" — defeats the point. Most pages have cacheable parts.
Pricing gates — quick reference
- Default cache, basic Cache Rules, Tiered Cache — all plans, including Free
- APO for WordPress — ~$5/mo add-on
- Cache Reserve — usage-based (storage + ops)
- Custom Cache Keys, Cache by Cookie, Cache by Header — Pro and above
- Caching on non-standard ports, Cache Tags, Origin Error Pass-through — Enterprise
- R2 — usage-based, zero egress fees